백엔드 - 게시판 CRUD

개요

떠밀린 개발.
나는 나중에 전부 lambda로 바꿀 것이다.
그러나 그 이전에는 일단 스프링으로 개발을 하는 경험도 섞어보자고.

미리 말하지만 이 프로젝트는 Comeet의 백엔드 팀원의 코드를 적극 보면서 클론 코딩하려고 한다.
스프링 문법을 많이 보긴 했어도 직접 써본 적은 없으니 적용 시 고려해야 할 사항에 대한 판단력이 낮다.
그래서 클론 코딩을 하면서 왜 이렇게 설계됐는지, 무슨 기능을 하는 것들이 있는지 탐구하면서 진행할 것이다.

API 설계

게시판 CRUD

Pasted image 20240712201218.png
게시판이다.
delete에는 바디를 넣을 수 없어서 삭제 메서드는 post로 했다.
전체 리스튼는 pageable 객체를 돌려주려고 한다.
상세 글 조회 시 댓글 리스트는 따로 api를 둘 것이다.
한 게시글의 길이가 길어질 수 있기 때문이다.
게시글 내용 자체는 S3에 저장하고 아이디만 오게 할 수도 있다.

댓글 CRUD

Pasted image 20240712232036.png
https://documenter.getpostman.com/view/11682851/TzJvex3F#1486fe97-2418-465b-8c37-e00797bafbd2
댓글은 보드에 종속적이도록 하는 것이 더 restful하다고 한다.
조금 고민인 지점은 그럼 컨트롤러가 하나로 고정되는 것 같고, 그렇게 되면 단일 책임 원칙을 지키기 힘들 것 같다.
아무리 restful이 중요하다고 해도..
gpt한테 문의하니 그래도 컨트롤러를 구분해낼 수는 있다고 하니, 그렇다면 괜찮다는 판단이 들이 들기는 한다.

디렉터리 구조 설정

Pasted image 20240714093444.png
과거 우리 팀들이 짜온 코드들을 조금 봤는데, CQRS를 시도하려고 디렉을 나누거나 DDD를 시도하는 흔적들이 많이 보였다.
그것 때문에 잠시 혼선이 있었지만, 나는 일단 과거에 가장 기본적으로 배웠던 레이어드 아키텍처 구조를 가져가기로 마음먹었다.
이름이 챗봇인 건.. 우리 팀을 팀으로 생각했을 적의 선택이었던..

엔티티 설정

Pasted image 20240714095446.png
먼저 베이스 엔티티를 만들어준다.
내가 잘 몰랐던 것들만 짚어보겠다.
https://velog.io/@kimdy0915/스프링-부트Spring-Boot로-게시판-벡엔드-서버-만들기
첫째 줄은 날짜와 같은 엔티티 관련 자동 설정을 도와주는 어노테이션.
여기에 메인 클래스에서도 수정 기능을 사용할 것을 명시해준다.
그리고 상위로서 상속받게될 클래스라는 것도 명시해준다.
Pasted image 20240714104912.png
게시판 엔티티.
기본키 설정과 댓글과의 연관 관계를 결정지었다.
아직은 이해가 부족해서 왜 하위 관계를 상위 클래스가 알아야 하는지는 모르겠다.
SQL에 직접적으로 지워진 건 조회하지 않도록 제한을 걸었다.
댓글 엔티티도 비슷하게 짰다.

다만 연관관계의 주인을 따지기 위해 일대다 중 다에 해당하는 댓글에는 JoinColumn을 넣었다.
OneToMany에서는 mappedBy를 쓰는데 이것은 나는 쟤의 이거랑 연관돼있다.. 정도를 의미한다고 한다.
연관관계가 있을 때는 외래키를 가지는 쪽이 주인이라고 한다.
주인이라는 표현이 잘 와닿지는 않지만, 나는 이렇게 이해했다.
두 테이블 간 연결점인 외래키 부분이 있다.
이 부분에 변화를 시킬 때 외래키를 가진 테이블은 상대에게 영향을 주지 않는다.
그러나 외래키를 제공해준 테이블의 변경은 상대에게 영향을 준다.
정확하게는 JPA에서 그렇게 세팅이 되어있다는 것이다.
그래서 영향을 받지 않는 자, 즉 외래키가 있는 쪽이 주인이 된다.
JPA는 주인을 기준으로 관계 상태를 관리한다고 한다.

DTO 제작

Pasted image 20240714122121.png
거의 모든 api에 대한 dto를 제작했다.
builder pattern을 사용했는데, 이게 어떻게 쓰이는지는 나중에 보면 좋을 것 같다.
딱 봤을 때 대충 느낌이 오기는 한다.
그러니까 서비스에서 board 인스턴스가 만들어지면 그대로 그것을 빌더 패턴을 구현한 메서드를 통해 만들어버리겠단 거겠지.
그런데 왜 static일까?

음, 이제부터는 틀을 잡았으니 역순으로 컨트롤러에서부터 구현을 들어가야 하나?
그게 감을 잡기에는 조금 더 편할 것 같다.
명시된 api를 토대로 dto를 만들었고, 이를 위해 앞서 엔티티를 명확히 정의했다.
이제 api에 매칭되는 컨트롤러를 만들고, 세부 로직을 구상하며 서비스를 구현하면 될 것 같다.

정확하게는, 메서드 하나씩 만들어가는 것이다.

전체 구조 잡기

Pasted image 20240714124342.png
이게 컨트롤러다.
https://dreamcoding.tistory.com/83
생성자 주입을 자동화해주기 위해 어노테이션을 달았다.
이렇게 하면 주입 방식은 생성자이면서도 직접 귀찮게 일일히 생성자에 명시를 하지 않아도 된다고 한다.
Pasted image 20240714124833.png
서비스는 이렇게 만들었다.
그런데 트랜잭셔널에 readonly는 넣는 게 맞나?
클래스, 메서드 간 트랜잭셔널 우선순위는 클래스라고 들었던 것 같은데, 이러면 쓰기 작업이 제대로 이뤄지나?
Pasted image 20240714122608.png
생각지 못한 이슈.
아니 왜 이거 class로 만들어졌지;;
아무튼 이러면 기본 세팅은 다 끝났다고 생각한다.
이제부터 구현을 들어가는 것이다.

CRUD 구현

create

Pasted image 20240714192301.png
음. 그냥 쉽게 성공했다.
이건 그냥 JPA 레포지토리에서 기본 제공하는 구현체를 이용하면 된다.

get

이게 이제 조금 헬이다.
Pasted image 20240714194436.png
내 입맛대로 커스텀한 레포지토리 메서드가 필요하다.
직접 구현해야지 뭐...
처음에는 그냥 findAll 때리고 조건에 맞는 것들을 거를까도 싶었는데, 아무리 생각해도 그건 말이 안 된다.
일단 모든 데이터를 다 쿼리해온다고?
누가 그런 비효율적인 로직을 짜나?
처음부터 잘 쿼리해왔어야지.
그러려면 커스텀이 답이다.
Pasted image 20240721231814.png
의존성 추가세팅부터 어려운데, 여기부터는 사실 친구의 도움을 완전히 받았다.
거의 강의를 듣다온 수준..
아무튼 조건에 따라 동적으로 쿼리를 날려야 하기 때문에 querydsl을 사용한다.
라이브러리가 잘 돼있어서, 적절하게 null 세팅만 잘해주면 알아서 해당 조건을 제외하고 쿼리를 날린다.
projections를 통해 내가 쿼리를 통해 받아오고 싶은 값의 형태를 미리 지정할 수 있어 자동완성이 매우 편하다.
무엇보다 맘에 드는 건 offset..
이것의 경우에는 index가 정렬된 경우 더 최적화해서 빠르게 쿼리를 가져오는 방법이 존재한다고 하나, 나같은 비기너가 할 레벨은 아니라는 조언을 참고했다.
이렇게 커스컴 레포지토리를 만든다.
Pasted image 20240721232126.png
이렇게만 만들어두고 나면 사실 서비스쪽 로직이 놀랍도록 단순하다.

update

Pasted image 20240721232224.png
여기에서 유의해서 볼만한 것은 옵셔널 체크.
Rust를 공부하면서 초반에 봤던 개념이라 조금 익숙하다.
이것도 난이도는 거의 create 정도이다.
Pasted image 20240721232351.png
대신 조금 편하게 업데이트를 하기 위해 엔터티에 빌더 메서드를 만들었다.

delete

Pasted image 20240721232430.png
서비스 로직은 그다지 볼 게 없으나, 엔터티 쪽에서의 설정이 조금 중요하다.
내 서비스에는 인증 인가가 없는 관계로 삭제를 위해 비밀번호를 전달받는다.
그래서 애초에 받은 메서드 자체가 post인데, 그럼에도 jpa를 활용할 때 delete가 적용되도록 할 수 있다.
대신에 delete 메서드를 적용할 때 저 SQLDelete에 정의된 문구가 날아가도록 세팅하는 것이다.
그러면 내 로직 상에서는 delete라고 표기하여 의미를 분명히 하는 동시에 실제 디비에서 일어나는 로직을 통일시킬 수 있다.

여기까지는 거의 친구의 강의를 들으면서 진행했고, 이후 댓글은 내가 스스로 혼자 구현했다.
다만 친구가 보는 앞에서 계속 확인 받으면서 해서 시간을 조금 빠르게 가져갈 수 있었다.

참고

pageable
https://velog.io/@soluinoon/Spring-Pageable-파헤치기